<?php

class HttpException extends Exception {}

class RemoteHttpRequest {

  #region constants
  const HTTP_1_0 = '1.0';
  const HTTP_1_1 = '1.1';
  const POST = 'POST';
  const GET = 'GET';

  const AUTH_TYPE_PLAIN = 'PLAIN';
  const AUTH_TYPE_DIGEST = 'DIGEST';
  #endregion

  #region defaults
  const DEFAULT_HTTP_VER = RemoteHttpRequest::HTTP_1_1;
  const DEFAULT_REQUEST_METHOD = RemoteHttpRequest::POST;
  const DEFAULT_TIMEOUT = 60; // seconds
  const DEFAULT_AUTH_TYPE = RemoteHttpRequest::AUTH_TYPE_PLAIN;
  #endregion

  #region properties
  public $Url;
  public $Input;
  public $HttpVer  = RemoteHttpRequest::DEFAULT_HTTP_VER;
  public $Method   = RemoteHttpRequest::DEFAULT_REQUEST_METHOD;
  public $Timeout  = RemoteHttpRequest::DEFAULT_TIMEOUT;
  public $Login    = false;
  public $AuthType = RemoteHttpRequest::DEFAULT_AUTH_TYPE;
  public $ContentType;

  public $ParsedResponse;
  public $RawResponse;
  #endregion

  public function __construct($Url = null, $Input = null) {
    if($Url !== null) {
      $this->Url = $Url;
    }
    if($Input !== null) {
      $this->Input = $Input;
    }
  }

  /**
   * Function: ParseHttpResponse - parses a raw http response and returns an associative array of ResponseCode, Headers and Body
   *           where Headers is an associative array of header name => header value pairs; cookies are further seperated into an
   *           array. The body is left unmodified.
   *
   * @author Craig Harris
   * @param string $Response a HTTP compliant, entire raw response from a web server
   * @return array An associatively structured version of the response
   *
   */
  public static function ParseHttpResponse($Response) {
    if(!is_string($Response)) {
      throw new HttpException('Unparsable response');
    }
  	do {
  		list($HeaderBlock, $Body) = Explode("\r\n\r\n", $Response, 2);
  		$HeaderLines = Explode("\r\n",$HeaderBlock);

  		$HttpResponseLine = Array_Shift($HeaderLines);
  		$Matches = Array();
  		if (Preg_Match('@^HTTP/[0-9]\.[0-9] ([0-9]{3}) (.*)@',$HttpResponseLine, $Matches)) {
  			$ResponseCode = $Matches[1];
  			$ResponseHttpStatus = $Matches[2];
  		} else {
  			return Array('Status'=>'N', 'ErrorCode'=>'Unreadable', 'Reason'=>'Unparsable', 'Type'=>'HTTP', 'SubType'=>'Corrupt');
  		}
  	} while($ResponseCode[0] == '1' && ($Response = $Body)); // Ignore continue headers

  	$Headers = Array();
  	foreach ($HeaderLines as $HeaderLine) {
  		list($Header,$Value) = Explode(': ',$HeaderLine,2);
  		if($Header == 'Set-Cookie') {
  			$Headers[$Header] = Explode("\n", $Value);
  		} else {
  			$Headers[$Header] = $Value;
  		}
  	}

  	if(isset($Headers['Transfer-Encoding']) && $Headers['Transfer-Encoding']=='chunked') {
  		throw new HttpException('We recieved a chunked transfer-encoding, but we have no support for such!');
  		//$Body = DecodeChunkedTransfer($Body);
  	}

  	$Result = Array('Status'=>'Y', 'Code'=>$ResponseCode, 'HttpReason'=>$ResponseHttpStatus, 'Headers'=>$Headers, 'Body'=>$Body);
  	if($ResponseCode != '200' && $ResponseCode != '204') {
  		$Result['ErrorCode'] = $ResponseCode;
  		$Result['Reason']    = !empty($Result['HttpReason']) ? $Result['HttpReason'] : HttpStatus($ResponseCode);
  		$Result['Type']      = 'HTTP';
  		$Result['errMsg']    = 'Only status codes 200 & 204 are considered succesful';
  	}

  	//:TODO: error handling if no body or if invalid headers etc
  	return $Result;
  }

  public function ParseResponse() {
    if(!empty($this->RawResponse)) {
      $this->ParsedResponse = self::ParseHttpResponse($this->RawResponse);
      return $this->ParsedResponse;
    } else {
      throw new HttpException('No HTTP response to parse');
    }
  }

  /**
    * Function: Get transport, returns the transport type to use for making socket connections for a particular scheme
    *
    * @author Craig Harris
    * @param string $Scheme the scheme/protocol to get the transport type for
    * @return string The transport type OR false on failure
    */
  public static function GetTransport($Scheme) {
  	if($Scheme == 'http') {
  		return 'tcp';
  	} elseif($Scheme == 'https') {
  		return 'ssl';
  	} elseif($Scheme=='tcp' || $Scheme=='ssl' || $Scheme=='sslv2' || $Scheme=='sslv3') {
  		return $Scheme;
  	} else { //error, unsupported scheme/transport type
  		return false;
  	}
  }

  public static function HttpRequest($Url, $Input, $HttpVer=RemoteHttpRequest::DEFAULT_HTTP_VER, $Method=RemoteHttpRequest::DEFAULT_REQUEST_METHOD, $Timeout=RemoteHttpRequest::DEFAULT_TIMEOUT, $Login=false, $AuthType=RemoteHttpRequest::DEFAULT_AUTH_TYPE) {
    $Http = new RemoteHttpRequest();
    $Http->Url      = $Url;
    $Http->Input    = $Input;
    $Http->HttpVer  = $HttpVer;
    $Http->Method   = $Method;
    $Http->Timeout  = $Timeout;
    $Http->Login    = $Login;
    $Http->AuthType = $AuthType;
    return $Http->DoRequest();
  }

  /**
   * Function: Remoting socket function. Login, keepalive and xml off as standard
   *
   * This function executes a socket connection to the supplied server
   * on a port as specified and returns the server response.
   *
   * @author:             Craig Harris
   * @param string $url         : server & port to connect to including host and page
   * @param string $timeout     : Seconds till timeout registered
   * @param string $input       : Data to form body of request
   * @param string $method      : GET or POST (defaults to 'GET')
   * @param string $login       : 2 value associative array{ user => username, pass => password} or a string of form 'username:password' or false for a non auth server
   * @param string $keepalive   : Time to maintain the connection to host, false by default
   * @param string $soap_action : Soap action URI
   *
   * @return $ret : server response
   */
  function DoRequest() {

    if(!is_array($this->Url)) {
  	  $this->Url = parse_url($this->Url);
  	}
  	$ts = @$this->Url['scheme'];
  	if(!$ts) {
  		// url was not absolute including transport scheme / protocol
  		throw new HttpException('Remote HTTP request requires the scheme to be part of the URL - it is not safe to make assumptions here');
  	}
  	$Transport = RemoteHttpRequest::GetTransport($ts);

  	if(!in_array($Transport, stream_get_transports())) {
  		return Array('Status'=>'N', 'Type'=>'Transport', 'ErrorCode'=>$Transport, 'errMsg'=>"'{$Transport}' is not a registered transport type, either correct the scheme '{$ts}' in the URL or install appropriate support for it on this server");
  	}

  	$server = "{$Transport}://{$this->Url['host']}";
  	$host = $this->Url['host'];
  	$target = $this->Url['path'];
  	$port = isset($this->Url['port'])?$this->Url['port']:$this->Url['scheme']=='http'?80:443;

    #region Input/Data/Content
  	$getValues = '';

  	// may be a string (eg, xml)
  	if(is_array($this->Input)) {
  		if($this->Method == RemoteHttpRequest::GET) {
  			$getValues = '?';
  			foreach($this->Input as $name => $value) {
  				$getValues .= urlencode($name) . '=' . urlencode($value) . '&';
  			}
  			$getValues = substr( $getValues, 0, -1 );
  		} elseif($this->Method == RemoteHttpRequest::POST) {
  			$postValues = '';
  			foreach($this->Input as $name => $value) {
  				$postValues .= urlencode($name) . '=' . urlencode($value) . '&';
  			}
  			$postValues = substr( $postValues, 0, -1 );
  		} else {
  			$errMsg = 'Invalid method selected. Use GET or POST';
  			//debug_error(__LINE__.$errMsg);
  			return Array('Status'=>'N', 'errMsg'=>$errMsg, 'Type'=>'BadArguments');
  		}
  	} else {
      if(!$this->ContentType) {
  		  $ContentType = 'text/xml; charset=utf-8';
		  }
  		$postValues = $this->Input;
  	}
  	#endregion

    if(!isset($ContentType)) {
  	  $ContentType = $this->ContentType;
    }

  	if($this->Method == RemoteHttpRequest::POST && isset($this->Url['query'])) { // Account for posting to URLs which have Query Strings
  		$getValues = '?'.$this->Url['query'];
  	}

  	// Building HTTP request
  	if(empty($target)) {
  		$target = '/';
  	}
  	$request  = "{$this->Method} {$target}{$getValues} HTTP/{$this->HttpVer}\r\n";
  	$request .= "Host: {$host}\r\n";
  	$request .= 'User-Agent: Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.2.1) ';
  	$request .= "Gecko/20021204\r\n";

    #region Digest Authentication - seed request and authentication calculation
  	if($this->AuthType == RemoteHttpRequest::AUTH_TYPE_DIGEST) {
      if(!$this->Login) {
        throw new HttpException('Cannot do digest authentication without login details');
      }
      $DigestSeedResponse = RemoteHttpRequest::HttpRequest($this->Url, '', $this->HttpVer, $this->Method, $this->Timeout);
      $DigestSeeds = $DigestSeedResponse['Headers']['WWW-Authenticate'];
      $DigestRegex = '£Digest realm="(.+?)", domain="(.+?)", nonce="(.+?)", algorithm=[\w\d]+?, qop="(.+?)"£';
      $Matches = Array();
      if(!preg_match($DigestRegex, $DigestSeeds, $Matches)) {
        throw new HttpException('Failed to obtain digest seeds etc');
      } else {
        //var_dump($Matches);
        $Realm  = $Matches[1];
        $Domain = $Matches[2];
        $Nonce  = $Matches[3];
        $qop    = $Matches[4];

        if(is_array($this->Login)) {
          $Username = $this->Login['user'];
          $Password = $this->Login['pass'];
        } else {
          list($Username, $Password) = explode($this->Login, ':');
        }

        $nc = '00000001'; // :todo: allow for retries, thus incrementing this // hex!
        $CNonce = alphanumeric_rand(8);

        $HA1 = md5("{$Username}:{$Realm}:{$Password}");
        $HA2 = md5("{$this->Method}:{$target}");
        $DigestResponse = md5("{$HA1}:{$Nonce}:{$nc}:{$CNonce}:{$qop}:{$HA2}");

        $DigestHeader = <<< EODH
Authorization: Digest username="{$Username}",
                      realm="{$Realm}",
                      nonce="{$Nonce}",
                      uri="{$target}",
                      qop={$qop},
                      nc={$nc},
                      cnonce="{$CNonce}",
                      response="{$DigestResponse}"
EODH;

      }
    }
    #endregion

  	#region Authentication
  	if($this->Login) {
  		if(is_array($this->Login)) {
  			$Auth = $this->Login['user'].':'.$this->Login['pass'];
  		} else {
  			$Auth = $this->Login;
  		}
  		if($this->AuthType == RemoteHttpRequest::AUTH_TYPE_DIGEST) {
        $request .= $DigestHeader."\r\n";
      } else {
  		  $request .= 'Authorization: Basic '.Base64_Encode($Auth)."\r\n";
  		}
  	}
  	#endregion

  	//$request .= "Accept: text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q= 0.8\r\n";
  	$request .= "Accept: */*\r\n";
  	$request .= "Accept-Language: en-gb;q=0.75,en-us;q=0.66,en;q=0.50\r\n";
  	$request .= "Accept-Charset: ISO-8859-1, utf-8;q=0.66, *;q=0.66\r\n";
  	$request .= "Cache-Control: no-store\r\n";
  	$request .= "Connection: close\r\n";

  	if($this->Method == RemoteHttpRequest::POST) {
  		$length = strlen($postValues);
  		if(isset($ContentType)) {
  			$request .= "Content-Type: {$ContentType}\r\n";
  		} else {
  			$request .= "Content-Type: application/x-www-form-urlencoded\r\n";
  		}
  		$request .= "Content-Length: {$length}\r\n";
  		$request .= "\r\n";
  		$request .= $postValues;
  	}

  	$StartTime = MicroTime(true);

  	//echo "<pre>Opening socket to send {$request}</pre>";

  	if(!$socket  = @FSockOpen($server, $port, $ErrNo, $ErrStr, $this->Timeout/4)) {
  		$errMsg = "Failed to open socket to {$server}";
  		//debug_error($errMsg);
  		return Array('Status'=>'N', 'errMsg'=>$errMsg, 'ErrorCode'=>$ErrNo, 'Reason'=>$ErrStr, 'Type'=>'Communication', 'SubType'=>'Socket/Open');
  	} else {

  		// Set timeout value
  		stream_set_timeout($socket, $this->Timeout);

  		stream_set_blocking($socket, 0);


  		//echo "~~~~<pre>\n{$request}\n</pre>~~~"; // :debug:

  		//Transmit the request
  		if(!FWrite( $socket, $request )) {
  			$errMsg = "Failed to send data to socket on {$server}";
  			//debug_error($errMsg);
  			return Array('Status'=>'N', 'errMsg'=>$errMsg, 'Type'=>'Communication', 'SubType'=>'Socket/Write');
  		}

  		// Read the response as a data stream // Beware that IIS is buggy and prematurely closes some SSL connections! - hence the error supression
  		// The loop is a workaround for the lack of SSL Timeout's on Solaris.
  		$HttpReply = '';
  		$BlockSize = 1600; // bytes

  		while(!feof($socket)) {
  			if($Buffer = @FRead($socket, $BlockSize)) {
  				$HttpReply .= $Buffer;
  				uSleep(10000); // 10ms delay, don't want to hammer CPU, but data should already be available
  			} else {
  				uSleep(100000); // 0.1 seconds delay so as to not hammer the CPU
  			}
  			if(Time()-$StartTime > $this->Timeout) {
  				error_log($errMsg = 'Manually enforced timeout', 0);
  				//debug_error($errMsg);
  				$TimedOut = True;
  				break;
  			}

  		}
  		/* if(!@$TimedOut) { // :debug:
  		  //debug_error('Succesful Request Duration: '.(Time()-$StartTime).'seconds');
  		} */

  		if(!feof($socket) || @$TimedOut) {
  			$errMsg = 'The operation timed out reading the response';
  			//debug_error($errMsg);
  			return Array('Status'=>'N', 'errMsg'=>$errMsg, 'Type'=>'Communication', 'SubType'=>'Timeout');
  		}
  		$info = stream_get_meta_data($socket);
  		$closed = FClose($socket);

  		$TimeTaken = MicroTime(true) - $StartTime;

  		//Close the connection
  		if(!$closed) {
  			$errMsg = "WARNING: Could not close socket to {$server}:{$port}";
  			//debug_error($errMsg);
  			return Array('Status'=>'N', 'errMsg'=>$errMsg, 'Type'=>'Communication', 'SubType'=>'Socket/Close', 'Duration'=>$TimeTaken);
  		} elseif($info['timed_out']) { //Catch timed-out responses
  			$errMsg = 'Connection timed out according to stream metadata';
  			//debug_error($errMsg);
  			return Array('Status'=>'N', 'errMsg'=>$errMsg, 'Type'=>'Communication', 'SubType'=>'Timeout', 'Duration'=>$TimeTaken);
  		} else {
        $this->RawResponse = $HttpReply;
  			return $this->ParseResponse() + Array('Duration'=>$TimeTaken);
  		}
  	}
  }

}

  define('START_WITH_NUMBER', true);
  define('START_WITH_ANY', false);

/**
 * Generates random alphanumeric strings (0-9, a-z, A-Z)
 *
 */
function alphanumeric_rand($length=11, $start_with_num=START_WITH_ANY) {
	$digits = array('0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z','A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z');
	if($start_with_num) {
		$tmp = mt_rand(0,9);
	} else {
		$tmp = $digits[mt_rand(0,61)];
	}
	for($j=2; $j<=$length; $j++) {
		$tmp .= $digits[mt_rand(0,61)];
	}
	return $tmp;
}


?>